Skip to content

Let the gateway pin a model to one upstream deployment - #81

Merged
varunursekar merged 6 commits into
mainfrom
feat/gateway-model-alias
Aug 1, 2026
Merged

Let the gateway pin a model to one upstream deployment#81
varunursekar merged 6 commits into
mainfrom
feat/gateway-model-alias

Conversation

@varunursekar

@varunursekar varunursekar commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Why

A contestant cell in the harness-engineering-bench study, gpt-5.6-sol × codex, failed
on terminal-bench and nowhere else. It ships real candidates on officeqa r1/r2/r3 and
browsecomp-plus r1/r2/r3, so it is not a routing problem with the contestant.

The fault is invalid_encrypted_content, and it is deterministic rather than
load-dependent. An unqualified model group is load-balanced across upstream
deployments, and the Responses API's encrypted reasoning content is decryptable only by
the deployment that produced it. Replaying it against a sibling deployment fails on
every turn after the first.

Reproduced against the upstream proxy directly, with no gateway and no codex in the
path:

model id replays passed
gpt-5.6-sol (bare group) 0 of 8
azure_ai/gpt-5.6-sol (pinned deployment) 5 of 5

Asking for the pinned deployment fixes it — but codex cannot express that request.
It reduces a model id to its last path component before putting it on the wire, so
azure_ai/gpt-5.6-sol arrives as gpt-5.6-sol and we are back where we started. There
is no client-side fix available for this harness.

What this does

Adds model_aliases to an inference-gateway scope: a declared map from the name a
caller may request to the upstream deployment that serves it.

The ordering is the whole design, and it is deliberate:

upstream_model = scope.model_aliases.get(model, model)

This runs after the allow-list check, not before. allowed_models continues to
govern what the optimizer may ask for — and therefore what a cell's label means —
while model_aliases decides only which deployment answers. Rewriting before the
check would force the aliased name into allowed_models, and the allow-list would stop
describing the contestant.

Both names are recorded in the request log via a new aliased_from field, so a
substitution the caller cannot see is still visible to whoever reads the log later.

refused_params is keyed on upstream_model for both its read and its write, since
refusing a parameter is a property of the deployment rather than of the name used for
it. Keying the write on the requested name would never hit on the read path for an
aliased model, and the cache would re-discover the same refusal on every request.

Where it is configured

In terminal-bench/baseline/build.azure.yaml, in the producer scope only. The
evaluation and finalization scopes are untouched — they target grok, which was never
affected.

That file is a copy of build.yaml rather than an overlay, and it differs from it by
exactly the two alias lines. Edits to build.yaml must be mirrored, which is not
hypothetical: the first version of this change missed the finalization budget and
needed a follow-up commit to bring it back into line.

Also included, from the same investigation: terminal-bench's finalization budget is
sized like the rest of the suite.

Verification

  • Full suite green: 496 passed, 8 skipped.
  • New tests cover the alias reaching the gateway through model_dump, self-aliases
    being dropped rather than rejected, empty targets being refused, and — the case that
    matters for a shared grid config — alias keys that a given launch can never request,
    since allowed_models is templated per cell while the alias map is not.
  • Live: two gpt-5.6-sol × codex terminal-bench cells have been running on this config
    for 25 and 18 minutes with zero invalid_encrypted_content and zero 403s, where
    the old failures crashed within minutes.

Note on scope

This is a workaround at the right layer, not the root fix. The upstream remedy is
encrypted_content_affinity in the proxy's router_settings, exactly as the error text
prescribes. This change lets the study close its last gap without waiting on that.

🤖 Generated with Claude Code

Greptile Summary

Adds per-scope model aliases to pin caller-visible model names to specific upstream deployments.

  • Applies aliases only after the requested model passes the scope allow-list.
  • Preserves aliases while compiling build specifications into gateway configuration.
  • Records requested and upstream model names and keys parameter-refusal caching by upstream deployment.
  • Adds an Azure-specific Terminal-Bench configuration and aligns its finalization budget with the shared configuration.
  • Adds validation and tests for alias serialization, routing, logging, and configuration parity.

Confidence Score: 5/5

The PR appears safe to merge, with the alias preserved through compilation and applied only after the existing model allow-list check.

The changed routing path retains scope authorization, rewrites only admitted model names, forwards the effective deployment consistently, and has focused coverage for configuration propagation and runtime behavior.

Important Files Changed

Filename Overview
vero/src/vero/gateway/inference.py Applies model aliases after authorization, forwards the rewritten payload, records alias attribution, and associates refused parameters with the effective upstream model.
vero/src/vero/harbor/build/specs.py Adds validated model-alias configuration while permitting inert aliases required by templated grid configurations.
harness-engineering-bench/terminal-bench/baseline/build.azure.yaml Introduces an Azure-pinned Terminal-Bench variant whose only intended behavioral difference is the producer model alias.
harness-engineering-bench/terminal-bench/baseline/build.yaml Raises the finalization token budget to cover the configured test and validation rescoring workload.
vero/tests/test_v05_harbor_inference.py Verifies post-allow-list alias routing, direct-target denial, pass-through behavior, and request-log attribution.
vero/tests/test_v05_benchmark_configs.py Ensures the Azure build variant remains identical to the shared configuration except for its declared alias.
vero/tests/test_v05_harbor_build.py Covers alias validation, self-alias normalization, unreachable keys, and serialization into gateway configuration.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Caller requests model] --> B{Allowed by scope?}
    B -- No --> C[Return model_denied]
    B -- Yes --> D[Resolve model_aliases]
    D --> E[Apply cached refused parameters]
    E --> F[Send to pinned upstream deployment]
    F --> G[Record upstream model and aliased_from]
Loading

Reviews (1): Last reviewed commit: "Ignore Codex CLI local state" | Re-trigger Greptile

varunursekar and others added 6 commits July 31, 2026 19:51
The cap was 700M tokens for 216 held-out case-runs (36 test cases x3
attempts, plus a rescore_top_k=3 pass over 36 validation cases), which is
3.24M per case-run. OfficeQA and BrowseComp-Plus both sit at ~5.05M, so
Terminal-Bench was 36% tighter than its siblings for no reason recorded
anywhere.

Erring low is the expensive direction. An exhausted finalization budget
surfaces as an upstream 429, and the held-out scoring it starves is the
one thing a run exists to produce -- that is how OfficeQA lost a
re-score. The per-case budget is the real spend control; these caps only
need to stop a runaway.

Measured after the fact, the first eight cells used at most 72.4M tokens,
so 700M would in fact have sufficed and this change bought nothing on the
searches run so far. Keeping it anyway: the sizing rationale holds for a
full-length search, and the eight terminal-bench results already on disk
were produced against this value, so committing it is what makes them
reproducible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codex reduces a model id to its last path component, so a codex cell can never
put `azure_ai/gpt-5.6-sol` on the wire. It gets the unqualified group instead,
which LiteLLM load-balances across deployments -- and the Responses API's
encrypted reasoning content is decryptable only by the deployment that produced
it. Every turn after the first fails with invalid_encrypted_content. That killed
terminal-bench `gpt-5.6-sol x codex` on r2, r3 and r4, five attempts in all.

Measured against the live proxy on 2026-08-01, replaying one encrypted reasoning
item:

    gpt-5.6-sol            0 of 5 replays succeeded
    azure_ai/gpt-5.6-sol   5 of 5 replays succeeded

The gateway is the only place that can supply the name the client cannot, so add
`model_aliases` to InferenceBudgetSpec and apply it in the proxy handler.

Applied AFTER the allow-list check, which is the whole design. `allowed_models`
governs what the caller may ask for, and is therefore what makes "this cell ran
gpt-5.6-sol" a true statement; the alias governs only which deployment serves
it. Rewriting first would force the aliased name into allowed_models and the
allow-list would stop describing the contestant. A test asserts that naming an
alias target directly is still refused, so an alias cannot become a second way
past the allow-list.

Unreachable keys and self-aliases are tolerated rather than rejected. One build
config serves every cell of a grid with allowed_models templated per launch, so
a map that pins the right deployment for one optimizer necessarily carries keys
the other launches never request; the first draft errored on those and would
have failed the nine non-sol terminal-bench cells. Nothing is lost by allowing
them -- an alias can only fire for a model the allow-list already admitted.

The substitution is recorded as `aliased_from` in the request log, alongside the
existing `dropped_params`, on the same principle: a change the caller cannot see
has to be auditable. refused_params is now keyed on the upstream model, since
refusing a parameter is a property of the deployment rather than of the name the
caller used, and keying read and write differently would have stopped the cache
ever hitting for an aliased model.
Leaves build.yaml byte-identical for the nine terminal-bench cells that do not
need an alias, so their config is provably the one they already ran. Follows the
existing variant pattern (swe-atlas-qna/baseline/build.gpt54mini.yaml).

The cost is duplication: there is no include/extends mechanism for these YAMLs,
so build.azure.yaml is a 183-line copy differing in one block. That is a drift
hazard with no natural alarm -- divergence in a timeout or a budget would change
results and fail nothing, and the azure cell would quietly stop being comparable
to its nine siblings.

So the guard is the point of this commit, not the file. The new test parses both
documents, pops model_aliases from the variant, and asserts the remainder is
equal. Verified by injecting a one-character change (max_concurrency 24 -> 25),
which fails with the reason rather than a diff nobody reads. It also checks the
alias reaches the gateway for optimizer_model=gpt-5.6-sol and stays inert for
claude-opus-5.
Rebasing onto main brought 9a09b44, which raised terminal-bench's finalization
max_tokens from 700M to 1.1B. build.azure.yaml was copied before that and still
carried 700M, so the azure cell would have scored held-out against a budget 36%
tighter than its nine siblings -- the starvation 9a09b44 exists to prevent.

The drift guard caught it on its first live outing, which is the argument for
having written it.
# Conflicts:
#	vero/tests/test_v05_harbor_build.py
The Codex CLI checks out git worktrees under .codex/ for its own runs --
23MB of them in this checkout -- which showed up as untracked noise on every
status. Nothing under it is ours to version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@varunursekar
varunursekar merged commit 3192b20 into main Aug 1, 2026
10 checks passed
@varunursekar
varunursekar deleted the feat/gateway-model-alias branch August 2, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants